import { MaybePromise } from '@boticord/common'
import { ExecutionContext, Injectable, NotFoundException } from '@nestjs/common'
import { BoticordAbility } from '../casl/ability.type'
import { IPolicyHandler, PolicyHandler } from '../interfaces/policy-handler.interface'
import { Token } from '../models/token.model'
import { DataSource, FindOneOptions } from 'typeorm'
import { ErrorCodes } from '@boticord/consts/errors.enum'
import { Action } from '../casl/action.enum'
import { JwtPayloadDto } from '@boticord/auth/dto/jwt-payload.dto'

export interface IdentifiableModel {
  id: string
}

export interface FetchingPolicyOptions<T> {
  id: (ctx: ExecutionContext) => string
  options?: FindOneOptions<T>
  exception?: Error
}

export const fromParam = (name = 'id') => (ctx: ExecutionContext) => 
  ctx.switchToHttp().getRequest<{ params: Record<string, string> }>().params[name]

export const fromBody = (name = 'id') => (ctx: ExecutionContext) =>
  ctx.switchToHttp().getRequest<{ body: Record<string, string> }>().body[name]

export const fromJwt =
  <T extends keyof JwtPayloadDto>(name: T) => (ctx: ExecutionContext): JwtPayloadDto[T] | undefined =>
    ctx.switchToHttp().getRequest<{ jwt?: JwtPayloadDto }>().jwt?.[name]

export const fromResponse = (...names: string[]) => (ctx: ExecutionContext, response: any) => {
  return names.reduce((acc, name) => {
    acc[name] = response[name]
    return acc
  }, {})
}

export const fromResponseSingle = (name = 'id') => (ctx: ExecutionContext, response: any) => {
  return fromResponse(name)(ctx, response)[name]
}

export const defaultFetchingPolicyOptions: FetchingPolicyOptions<any> = {
  id: fromParam(),
  exception: new NotFoundException({ code: ErrorCodes.NotFound }),
  options: {},
}

export const mergeOptions = <T>(options: Partial<FetchingPolicyOptions<T>>) => ({
  ...defaultFetchingPolicyOptions,
  ...options,
})

export type FetchingPolicyHandler<T extends IdentifiableModel> = 
  (resource: T, ability: BoticordAbility, context: ExecutionContext, token?: Token) => MaybePromise<boolean>

export type SimpleFetchCheckHandler<T extends IdentifiableModel> = 
  (res: T, ability: BoticordAbility) => MaybePromise<boolean>

export const SimpleFetchCheck = 
  <T extends IdentifiableModel>(action: Action): FetchingPolicyHandler<T> =>
    (res, ability) => ability.can(action, res)

// fetches given resource using extractID and passed it to handler.
// ex: FetchingPolicy(Bot, (res, ability) => ability.can(Action.Create, res))
// ex2: ex: FetchingPolicy(Bot, SimpleFetchCheck(Action.Create))
export const FetchingPolicy = 
  <T extends IdentifiableModel>(
    resource: new(...args: any[]) => T, 
    handler: FetchingPolicyHandler<T>, 
    options: Partial<FetchingPolicyOptions<T>> = {},
  ): PolicyHandler => {
      const opts = mergeOptions(options)

      @Injectable()
      class Inner implements IPolicyHandler {
        constructor(
          private readonly source: DataSource,
        ) {}

        async handle(ability: BoticordAbility, context: ExecutionContext, token?: Token) {
          const repo = this.source.getRepository(resource)
          const id = opts.id(context)

          // @ts-ignore
          let model = await repo.findOne({ where: { id }, ...options.options })

          if (!model) {
            // @ts-ignore
            model = await repo.findOne({ where: { shortLink: id }, ...options.options })
          }

          if (model === null) {
            throw opts.exception ?? new NotFoundException({ code: ErrorCodes.NotFound })
          }

          return handler(model, ability, context, token)
        }
      }

      return Inner as any
    }
